import datetime
import arrow
import json
from functools import wraps

import requests
from xml.etree import ElementTree
from flask import current_app, g
from pymongo import UpdateOne
from bson import ObjectId

from app.db.models.balances import Balances
from app.db.models.packages import Packages
from app.db.models.users import User, Role
from app.db.models.alert_groups import AlertGroup
from app.db.models.alertreceivers import AlertReceivers
from app.db.models.charges import Charges
from app.db.models.meta_limit import ExpTypeLimit
from app.handler.v2_charges import ChargesHelper
from app.errors import ServerError, APIException
from app.libs.enums import ChargeType, LimitType
from app.libs.utility import get_period_stamp, filter_cha, merge_feature
from app.libs.cas_api import request_cas
from app.libs.regexp import PHONE_RE, USER_EMAIL_RE
from app.config.settings import CAS_CONFIG, UA, USER_INIT_PKG_KEY, FREE_PKG_KEY

number_limit_type = (LimitType.limit.value, LimitType.period.value, LimitType.max.value)


class UserHelper(object):
    def __init__(self, request_args, use_plan=False, only_one=False):
        uid = request_args.get('uid')
        username = filter_cha(request_args.get('username', ''))
        plan_name = filter_cha(request_args.get('plan_name', ''))
        uid_list = request_args.get('uids', [])

        self.use_plan = use_plan

        self.only_one = only_one
        self._data = None
        self.query = {}
        self.plan_query = {}

        if uid:
            self.only_one = True
            self.query['_id'] = uid if isinstance(uid, ObjectId) else ObjectId(uid)
        if uid_list:
            self.query['_id'] = {'$in': uid_list}
        if username:
            self.query['username'] = {'$regex': username}
        if use_plan and plan_name:
            self.plan_query['plans.0.name'] = {'$regex': plan_name}

        self._get()

    def _get(self):
        pipeline = [
            {'$match': self.query},
            {'$lookup': {'from': "balances", 'localField': "_id", 'foreignField': "uid", 'as': "balances"}},
            {'$lookup': {'from': 'packages', 'localField': 'balances.0.planId', 'foreignField': '_id', 'as': 'plans'}},
            {'$match': self.plan_query}
        ]
        users = User.objects.aggregate(pipeline)
        data = []
        for user in users:
            if _balances := user.get('balances', []):
                _balances = _balances[0]
                user['balances'] = Balances.from_dict(_balances)

            if _plan := user.get('plans', []):
                _plan = _plan[0]
                user['plan_name'] = _plan.get('name', '')

            data.append(user)

        self._data = data

    def get_data(self):
        if self._data is None:
            self._get()

        if self.only_one and self._data:
            return self._data[0]

        return self._data


def add_balance_check(item, o_uid, error_list, balance, pack_plan=None, plan_item: dict = None):
    pack_id = item.get('chargeId')
    charge_type = item.get('chargeType')

    if not (pack := Packages.objects.filter(pk=pack_id, chargeType=charge_type).first()):
        error_list.append(f"{pack_id}套餐/模块不存在.")

    if charge_type == ChargeType.addition_package.value:
        if pack_plan is None:
            if (checked_feature := check_feature_is_manual(pack['feature'])) is not None \
                    and checked_feature != check_feature_is_manual(balance['feature']):
                error_list.append(f"模块 '{pack.name}' 的类型不适用于用户套餐!")

            if not max(balance['feature'].to_dict().values()):
                raise ServerError(msg="免费版不能充值模块.")
        else:
            if (checked_feature := check_feature_is_manual(pack['feature'])) is not None \
                    and checked_feature != check_feature_is_manual(pack_plan['feature']):
                error_list.append(f"模块 '{pack.name}' 的类型不适用于用户套餐!")
        if (not balance.planExpireTime) and (not plan_item.get("expireTime")):
            raise ServerError(msg="没有购买套餐或者没有设置套餐过期时间.")
        plan_expire_time = balance.planExpireTime + datetime.timedelta(hours=8) if balance.planExpireTime else arrow.get(plan_item.get("expireTime"))
        if pack_plan and plan_item:
            plan_expire_time = max([arrow.get(plan_expire_time), arrow.get(plan_item.get("expireTime"))])
        if plan_expire_time.timestamp() < arrow.get(item.get("expireTime")).timestamp():
            raise ServerError(msg="模块过期时间不能大于套餐过期时间.")

    for charge in ChargesHelper({'uid': o_uid, 'chargeType': ChargeType.plan.value}).get():
        if charge.limitType != LimitType.limit.value:
            continue
        _balance_content = balance.balance
        _func_type = charge.funcType
        _new_total = getattr(_balance_content, _func_type).total - charge.total + getattr(pack.limit, _func_type).total
        if getattr(_balance_content, _func_type).used > _new_total:
            raise ServerError(msg='已使用余额超出当前充值套餐可用值')

    return pack_plan


def give_plan(uid, pack, charge_num, expire_time):
    refresh_datetime = get_period_stamp()
    o_uid = ObjectId(uid)
    now_datetime = arrow.utcnow().datetime
    balance = Balances.objects.filter(uid=uid).first()

    charges = ChargesHelper({'uid': o_uid, 'chargeType': ChargeType.plan.value}).get()

    charges_map = {
        item['funcType']: item
        for item in charges
    }
    insert_charges = []
    balance_update_dict = {}
    for func_type in balance.balance:
        if old_charge := charges_map.get(func_type):
            if old_charge.limitType in (LimitType.limit.value, LimitType.period.value, LimitType.max.value):
                if old_charge.limitType != LimitType.limit.value:
                    _temp_key_used = f"inc__balance__{func_type}__used"
                    balance_update_dict[_temp_key_used] = balance_update_dict.get(_temp_key_used, 0) - old_charge.used
                _temp_key_total = f"inc__balance__{func_type}__total"
                balance_update_dict[_temp_key_total] = balance_update_dict.get(_temp_key_total, 0) - old_charge.total

        if (_pack_item := getattr(pack.limit, func_type, None)) and _pack_item.total != 0:
            _limit_type = _pack_item.limitType
            _real_total = get_real_total(_limit_type, _pack_item.total, _pack_item.number)

            _charge_item = {
                'createTime': now_datetime,
                'updateTime': now_datetime,
                'packId': pack.id,
                'taskType': _pack_item.taskType,
                'limitType': _limit_type,
                'total': _real_total,
                'expireTime': expire_time,
                'uid': ObjectId(uid),
                'number': charge_num,
                'funcType': func_type,
                'chargeType': pack.chargeType
            }

            if _limit_type == LimitType.period.value:
                _charge_item['time'] = refresh_datetime

            if _limit_type != LimitType.frequency.value:
                _temp_key = f"inc__balance__{func_type}__total"
                balance_update_dict[_temp_key] = balance_update_dict.get(_temp_key, 0) + _real_total

            insert_charges.append(Charges().from_dict(_charge_item))

    balance_update_dict['planId'] = pack.id
    balance_update_dict['planExpireTime'] = expire_time
    balance_update_dict['planChargeTime'] = now_datetime

    balance_update_dict["set__balance__alertGroup__used"] = AlertGroup.objects.filter(uid=uid).count()
    balance_update_dict["set__balance__alertReceiver__used"] = AlertReceivers.objects.filter(uid=uid).count()

    charges.update(isDelete=True)
    if insert_charges:
        Charges.objects.insert(insert_charges)
    if balance_update_dict:
        balance.update(**balance_update_dict)
    User(pk=uid).update(planId=pack.id)


def give_addition_package(uid, pack, charge_num, expire_time):
    now_datetime = arrow.utcnow().datetime
    refresh_datetime = get_period_stamp()
    balance = Balances.objects.filter(uid=uid).first()

    insert_charges = []
    balance_update_dict = {}
    delete_func_type = []
    for func_type in balance.balance:
        _pack_item = getattr(pack.limit, func_type, None)
        if _pack_item and (_pack_item.total != 0 or _pack_item.limitType == LimitType.exp.value):
            if _pack_item.limitType in [LimitType.exp.value, ] and not getattr(pack.feature, func_type, False):
                continue
            _limit_type = _pack_item.limitType
            _real_total = get_real_total(_limit_type, _pack_item.total, charge_num)
            _charge_item = {
                'packId': pack.id,
                'taskType': _pack_item.taskType,
                'limitType': _limit_type,
                'total': _real_total,
                'expireTime': expire_time,
                'uid': ObjectId(uid),
                'number': charge_num,
                'funcType': func_type,
                'chargeType': pack.chargeType,
                'createTime': now_datetime,
                'updateTime': now_datetime,
            }

            if _limit_type == LimitType.period.value:
                _charge_item['time'] = refresh_datetime

            if _limit_type == LimitType.exp.value:
                _charge_item['time'] = arrow.get(expire_time).timestamp()
                _pack_item.time = arrow.get(expire_time).timestamp()
                balance_update_dict[f"set__balance__{func_type}"] = _pack_item
                # 按照持续时间结算的模块， 只保留最新一次的购买记录有效期
                delete_func_type.append({"funcType": func_type, "packId": pack.id})

            if _limit_type not in [LimitType.frequency.value, LimitType.exp.value]:
                balance_update_dict[f"inc__balance__{func_type}__total"] = _real_total
            insert_charges.append(Charges().from_dict(_charge_item))
    if delete_func_type:
        for query in delete_func_type:
            Charges.objects.filter(uid=uid, **query).update(isDelete=True)
    if insert_charges:
        Charges.objects.insert(insert_charges)
    if balance_update_dict:
        balance.update(**balance_update_dict)


def recharge_used(uid):
    now_datetime = arrow.utcnow().datetime
    recharge_limit_type = (LimitType.limit.value, LimitType.period.value, LimitType.max.value)
    if not (user := UserHelper({'uid': uid}).get_data()):
        raise ServerError(msg=f"用户'{uid}' 不存在.")

    balance = Balances.objects.filter(uid=uid).first()

    pipeline = [
        {'$match': {'uid': user.get('_id'), 'isDelete': False, 'limitType': {'$in': recharge_limit_type}}},
        {'$sort': {'expireTime': 1}},
        {'$sort': {'limitType': -1}},
        {'$group': {
            '_id': '$funcType',
            'charges': {'$addToSet': {'_id': '$_id', 'used': '$used', 'total': '$total'}}
        }}
    ]

    update_list = []
    for item in Charges.objects.aggregate(pipeline):
        func_type = item['_id']
        charges = item['charges']
        is_exhaust = False
        balance_func = getattr(balance.balance, func_type)
        _used = balance_func.used

        for i, charge in enumerate(charges):
            if is_exhaust:
                update_list.append(UpdateOne(
                    {'_id': charge.get('_id')}, {'$set': {'used': 0, 'updateTime': now_datetime}}))
                continue
            if i + 1 == len(charges):
                update_list.append(UpdateOne(
                    {'_id': charge.get('_id')}, {'$set': {'used': _used, 'updateTime': now_datetime}}))
                break
            if (r := _used - charge.get('total', 0)) <= 0:
                update_list.append(UpdateOne(
                    {'_id': charge.get('_id')}, {'$set': {'used': _used, 'updateTime': now_datetime}}))
                is_exhaust = True
            else:
                update_list.append(UpdateOne(
                    {'_id': charge.get('_id')}, {'$set': {'used': charge.get('total', 0), 'updateTime': now_datetime}}))
                _used = r

    if update_list:
        getattr(Charges, '_get_collection')().bulk_write(update_list)


def calculate_feature(o_uid):
    pipeline = [
        {'$match': {'uid': o_uid, 'isDelete': False}},
        {'$group': {'_id': '$packId'}}
    ]
    id_list = [item['_id'] for item in Charges.objects.aggregate(pipeline) if item['_id']]
    feature = {}
    for item in Packages.objects.filter(pk__in=id_list):
        if not feature:
            feature = item.feature.to_dict()
        else:
            feature = merge_feature(feature, item.feature.to_dict())
    balance = Balances.objects.filter(uid=o_uid).first().balance.to_mongo().to_dict()
    for k, item in balance.items():
        if item['limitType'] == LimitType.exp.value and item.get("time") > arrow.utcnow().timestamp():
            feature[k] = True
        elif item['limitType'] == LimitType.exp.value and item.get("time") < arrow.utcnow().timestamp():
            feature[k] = False
    return feature


def calculate_freq(o_uid):
    freq_dict = {}
    for charge in ChargesHelper({'uid': o_uid, "limitType": LimitType.frequency.value}).get():
        func_type = charge.funcType
        freq_dict.setdefault(func_type, []).append(charge.total)

    update_dict = {}
    for k, v in freq_dict.items():
        if not v:
            continue
        if "Freq" in k:
            func = min
        else:
            func = max

        update_dict[f"set__balance__{k}__total"] = func(v)

    return update_dict


def check_feature_is_manual(feature: dict):
    contain_task = False
    for k in feature:
        if 'Manual' in k and feature[k]:
            _temp_k = k
            k.replace('Manual', '')
            if k == "ipv6ManualTask":
                continue
            if not feature[k.replace('Manual', '')]:
                return True
        if 'Task' in k and feature[k]:
            contain_task = True

    if not contain_task:
        return None

    return False


def get_real_total(limit_type, total, num) -> int:
    if num < 1:
        num = 1
    if limit_type in number_limit_type:
        _real_total = total * num
    else:
        _real_total = total

    return _real_total


class LoginHelper(object):

    @staticmethod
    def check_sso_is_exists(username):
        data = {
            "url": f"/accounts/{CAS_CONFIG['appid']}/verify/",
            "body": {"username": username}
        }
        flag, res = request_cas(data)
        if not flag:
            return False
        if not isinstance(res, dict):
            return False
        return res.get("result", False)

    @staticmethod
    def check_easm_is_exists(username):
        user = User.objects.filter(username=username).first()
        return user

    @staticmethod
    def parse_user_data(data):
        username = None
        attributes = {}
        tree = ElementTree.fromstring(data)
        if tree[0].tag.endswith('authenticationSuccess'):
            for element in tree[0]:
                if element.tag.endswith('user'):
                    username = element.text
                elif element.tag.endswith('attributes'):
                    for attribute in element:
                        tag = attribute.tag.split("}").pop()
                        if tag in attributes:
                            if isinstance(attributes[tag], list):
                                attributes[tag].append(attribute.text)
                            else:
                                attributes[tag] = [attributes[tag]]
                                attributes[tag].append(attribute.text)
                        else:
                            attributes[tag] = attribute.text
        return username, attributes

    @staticmethod
    def validate_ticket(ticket, service):
        url = f'{CAS_CONFIG["base_url"]}/cas/proxyValidate?ticket={ticket}&service={service}'
        resp = requests.get(url, headers={"user-agent": UA}, verify=bool(CAS_CONFIG.get("is_verify_ssl")))
        if not resp.ok:
            current_app.logger.warning(f"登录解析ticket失败, {resp.text}")
            raise APIException(msg="登录错误")
        return LoginHelper.parse_user_data(resp.content.decode())

    @staticmethod
    def create_user(username):
        if not (role := Role.objects.find({"name": "普通用户"}).first()):
            current_app.logger.error(f"创建用户，缺少初始化角色")
            raise APIException(msg="系统错误， 缺少初始化数据")
        user_data = {
            "type": "cas",
            "roleIds": [role],
            "username": username,
        }
        if not (package := Packages.objects.find({"key": USER_INIT_PKG_KEY}).first()):
            current_app.logger.error(f"创建用户，缺少初始化套餐")
            raise APIException(msg="系统错误， 初始化数据")
        is_email = True
        if PHONE_RE.fullmatch(username):
            is_email = False
            user_data.update({"phone": username})
        elif USER_EMAIL_RE.fullmatch(username):
            user_data.update({"email": username})
        else:
            raise APIException(msg="用户信息错误, 用户名只能是邮箱或者11位电话号码")
        user = User(**user_data).save()
        alert_receiver = AlertReceivers(
            enable=True,
            isDefault=True,
            name="默认告警人",
            uid=user.id
        )
        if is_email:
            alert_receiver.email = username
        else:
            alert_receiver.phone = username
        alert_receiver.save()
        alert_group = AlertGroup(
            isDefault=True,
            uid=user.id,
            name="默认组",
            enable=True,
            includeWechat=False,
            receiverIds=[alert_receiver.id]
        ).save()
        balance = Balances(
            uid=user.id,
            planId=package.id,
            planChargeTime=datetime.datetime.utcnow()
        )
        if USER_INIT_PKG_KEY != FREE_PKG_KEY:
            balance.planExpireTime = datetime.datetime.utcnow() + datetime.timedelta(days=365)
        balance.balance.alertReceiver.used = 1
        balance.balance.alertGroup.used = 1
        balance = balance.save()
        user.balanceId = balance.id
        user.planId = package.id
        user = user.save()
        Charges(funcType='alertReceiver', limitType=LimitType.limit.value, used=1, chargeType='plan', uid=user.id).save()
        Charges(funcType='alertGroup', limitType=LimitType.limit.value, used=1, chargeType='plan', uid=user.id).save()
        current_app.logger.info(f"创建用户，{user.username}")
        return user

    @staticmethod
    def update_user_info(user, info):
        update_data = {
            "lastActiveTime": arrow.utcnow().datetime
        }
        if info:
            update_data.update({
                "phoneVerified": LoginHelper.parse_user_info(info, "phone_is_active", True),
                "emailVerified": LoginHelper.parse_user_info(info, "email_is_active", True),
                "identified": LoginHelper.parse_user_info(info, "identity_is_active", True),
                "enableMfa": LoginHelper.parse_user_info(info, "mfa", True),
                "registerTime": arrow.get(info.get("date_joined")).datetime,
            })
            if email := json.loads(info.get("safe_email", "{}")).get("email", ""):
                update_data.update({"email": email})
            idcard_num = LoginHelper.parse_user_info(info, "idcard_num", False)
            if idcard_num:
                idcard_num = idcard_num[:6] + "********" + idcard_num[14:]
                update_data.update({"idCardNum": idcard_num})
            for s in ["realname", "attribution", "qq", "address", "phone"]:
                if value := LoginHelper.parse_user_info(info, s, False):
                    update_data.update({s: value})
        user.update(**update_data)

    @staticmethod
    def parse_user_info(user_info, field, is_bool=False):
        if is_bool:
            return True if user_info.get(field, False) not in ["False", False, "None"] else False
        else:
            return user_info.get(field, "") if user_info.get(field, "") not in ["False", False, "None"] else ""


def agree_user_protocol(user, allow_protocol):
    if user.disclaimerAgreeTime:
        return True, "您已经签署了《知道创宇ScanV服务条款》"
    if not allow_protocol:
        return False, "您没有同意签署《知道创宇ScanV服务条款》，不能为你提供服务"
    user.update(disclaimerAgreeTime=datetime.datetime.utcnow())
    return True, "您成功签署了《知道创宇ScanV服务条款》"


def check_user_pkg(user):

    if user.roleIds and user.roleIds[0].name == "管理员":
        return True
    free_pkg = Packages.objects.filter(key=USER_INIT_PKG_KEY).first()
    balance = Balances.objects.filter(uid=user.id).first()
    if (balance.planId and balance.planId != free_pkg.id) and (
            balance.planExpireTime and arrow.get(balance.planExpireTime).datetime > arrow.utcnow().datetime):
        return True
    return False


def check_user_free_pkg(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        if not check_user_pkg(g.user):
            raise APIException(msg="套餐不支持")
        return func(*args, **kwargs)
    return decorated_function
